// STL
#include <fstream>  // std::ifstream
#include <iostream> // std::cerr
#include <string>   // std::string
#include <tuple>    // std::tuple<>, ::make_tuple()

// Boost
#include <boost/program_options.hpp>   // boost::program_options


//
// DESC: Read the parameters file.
//
std::tuple<
           std::string,         // ensemble_file
           // =====
           // [mlp] BLOCK
           // -----
           std::string,         //  mlp_file
           // -----
           std::string,         //  filename_mlp_create
           // -----
           std::string,         //  mlp_train_X_in_file
           std::string,         //  mlp_train_X_out_file
           std::string,         //  filename_mlp_train
           // =====
           // [create] BLOCK
           // -----
           bool,                // create
           // -----
           int,                 // nmlp
           // -----
           bool,                // is_classif
           // -----
           int,                 // ni
           int,                 // no
           // =====
           // [create] BLOCK
           // -----
           bool,                // train
           // -----
           std::string,         //  train_X_in_file
           std::string,         //  train_X_out_file
           // -----
           std::string,         //  train_method
           // =====
           // [optimize] BLOCK
           // -----
           bool,                 // optimize
           // -----
           std::string,          // optimize_X_in_file
           std::string,          // optimize_X_out_file
           // -----
           std::string,          // optimize_method
           // -----
           double,               // Ensemble_gen_err_eps
           // -----
           int,                  // BMC_N
           // =====
           // [test] BLOCK
           // -----
           bool,                 // test
           // -----
           std::string,          // test_X_in_file
           std::string,          // test_X_out_file
           // -----
           double,               // cutoff
           // =====
           std::string           // prefix
           > read_param_file(
                             const std::string param_filename
                             )
{
    namespace po = boost::program_options;

    //=========================================================

    // ----- PARAMETERS -----
    std::string ensemble_file;       // (always) required
    // =====
    // [mlp] BLOCK
    // -----
    std::string mlp_file;
    // -----
    std::string filename_mlp_create;
    // -----
    std::string mlp_train_X_in_file;
    std::string mlp_train_X_out_file;
    std::string filename_mlp_train;
    // =====
    // [create] BLOCK
    // -----
    bool        create               = false;
    // -----
    int         nmlp;
    // -----
    bool        is_classif;
    // -----
    int         ni;
    int         no;
    // =====
    // [train] BLOCK
    // -----
    bool        train               = false;
    // -----
    std::string train_X_in_file;
    std::string train_X_out_file;
    // -----
    std::string train_method;
    // =====
    // [optimize] BLOCK
    // -----
    bool        optimize            = false;
    // -----
    std::string optimize_X_in_file;
    std::string optimize_X_out_file;
    // -----
    std::string optimize_method;
    // -----
    double      Ensemble_gen_err_eps;
    // -----
    int         BMC_N;
    // =====
    // [test] BLOCK
    // -----
    bool        test                = false;
    // -----
    std::string test_X_in_file;
    std::string test_X_out_file;
    // -----
    double      cutoff;
    // =====
    std::string prefix;             // (always) required

    //=========================================================

    try
    {
        //=========================================================
        // SET THE OPTIONS
        //=========================================================

        po::options_description desc("options");

        desc.add_options()
        ("ensemble_file",            po::value<std::string>(&ensemble_file)->required(),                "Ensemble filename (input or output)")
        // =====
        // [mlp] BLOCK
        // -----
        ("mlp.mlp_file",             po::value<std::string>(&mlp_file),                                 "[mlp] mlp file")
        // -----
        ("mlp.mlp_create_file",      po::value<std::string>(&filename_mlp_create),                      "[mlp] mlp_create file")
        // -----
        ("mlp.mlp_train_X_in_file",  po::value<std::string>(&mlp_train_X_in_file),                      "[mlp] mlp train X_in file")
        ("mlp.mlp_train_X_out_file", po::value<std::string>(&mlp_train_X_out_file),                     "[mlp] mlp train X_out file")
        ("mlp.mlp_train_file",       po::value<std::string>(&filename_mlp_train),                       "[mlp] mlp_train file")
        // =====
        // [create] BLOCK
        // -----
        ("create.nmlp",              po::value<int>(&nmlp),                                             "[create] number of MLPs")
        // -----
        ("create.classif",           po::value<bool>(&is_classif),                                      "[create] is classification")
        // -----
        ("create.ni",                po::value<int>(&ni),                                               "[create] number of input")
        ("create.no",                po::value<int>(&no),                                               "[create] number of output")
        // =====
        // [train] BLOCK
        // -----
        ("train.X_in_file",          po::value<std::string>(&train_X_in_file),                          "[train] train X_in file")
        ("train.X_out_file",         po::value<std::string>(&train_X_out_file),                         "[train] train X_out file")
        // -----
        ("train.method",             po::value<std::string>(&train_method),                             "[train] training method")
        // =====
        // [optimize] BLOCK
        // -----
        ("optimize.X_in_file",       po::value<std::string>(&optimize_X_in_file),                       "[optimize] optimization X_in file")
        ("optimize.X_out_file",      po::value<std::string>(&optimize_X_out_file),                      "[optimize] optimization X_out file")
        // -----
        ("optimize.method",          po::value<std::string>(&optimize_method),                          "[optimize] optimization method")
        // -----
        ("optimize.Ensemble_gen_err_eps",  po::value<double>(&Ensemble_gen_err_eps)->default_value(1.e-8), "[optimize] Ensemble gen. err. eps")
        // -----
        ("optimize.BMC_N",           po::value<int>(&BMC_N)->default_value(1000),                       "[optimize] BMC N")
        // =====
        // [test] BLOCK
        // -----
        ("test.X_in_file",           po::value<std::string>(&test_X_in_file),                           "[test] test X_in file")
        ("test.X_out_file",          po::value<std::string>(&test_X_out_file),                          "[test] test X_out files")
        // -----
        ("test.cutoff",              po::value<double>(&cutoff),                                        "[test] cutoff for binary classification")
        // =====
        ("prefix",                   po::value<std::string>(&prefix)->required(),                       "prefix for output")
        ;

        //=========================================================
        // READ THE FILE
        //=========================================================

        std::ifstream ifs(param_filename, std::ios::in);

        po::variables_map vm;
        po::store(po::parse_config_file(ifs , desc), vm);
        po::notify(vm);

        ifs.close();

        //=========================================================
        // INFER CALCULATION TYPE(S)
        //=========================================================

        // TODO: it would be nice if we could determine create / train / etc. by just checking if a block (e.g., "[train]" was specified)

        // ----- CREATE -----
        if(
           vm.count("create.nmlp")
           )
        {
            create = true;
        }

        // ----- TRAIN -----
        if(
           vm.count("train.method")
           )
        {
            train = true;
        }

        // ----- OPTIMIZE -----
        if(
           vm.count("optimize.method")
           )
        {
            optimize = true;
        }

        // ----- TEST -----
        if(
           vm.count("test.X_in_file")
           )
        {
            test = true;
        }

/*
        //=========================================================
        // CONSISTENCY / ERROR CHECK
        //=========================================================
        // NOTE: default vectors are also set here

        // TODO: it would be nice to determine interrelations between parameters, rather than all of the messy if checks below
        // TODO: ... but I am not sure that is possible with Boost program_options

        //---------------------------------------------------------
        // CREATE
        //---------------------------------------------------------
        if(create_nunits.size() != create_units_type.size())
        {
            std::cerr << "error in read_param_file(): create_nunits.size() != create_units_type.size()" << '\n';
            //        return false;
            exit(0);
        }

        if(create)
        {
            if(create_nunits.empty())
            {
                std::cerr << "error in read_param_file(): create specified, but create_nunits.empty()" << '\n';
                //        return false;
                exit(0);
            }
        }

        //---------------------------------------------------------
        // TRAIN
        //---------------------------------------------------------
        if(train)
        {
            if(train_X_file.empty())
            {
                std::cerr << "error in read_param_file(): create specified, but train_X_file.empty()" << '\n';
                //        return false;
                exit(0);
            }
        }

        // ----- LEARNING RATE ADJUSTMENTS -----
        if(train_lr_dot.size() != train_lr_adj.size())
        {
            std::cerr << "error in read_param_file(): train_lr_dot.size() != train_lr_adj.size()" << '\n';
            //        return false;
            exit(0);
        }

        if(train_lr_dot.empty())
        {
            train_lr_dot = std::vector<double>(2);
            train_lr_dot[0] = 0.5;
            train_lr_dot[1] = 0.3;
            train_lr_adj = std::vector<double>(2);
            train_lr_adj[0] = 1.05;
            train_lr_adj[1] = 1.01;
        }

        // ----- MOMENTUM ADJUSTMENTS -----
        if(train_p_dot.size() != train_p_adj.size())
        {
            std::cerr << "error in read_param_file(): train_p_dot.size() != train_p_adj.size()" << '\n';
            //        return false;
            exit(0);
        }

        if(train_p_dot.empty())
        {
            train_p_dot = std::vector<double>(1);
            train_p_dot[0] = 0.3;
            train_p_adj = std::vector<double>(1);
            train_p_adj[0] = 1.5;
        }

        //---------------------------------------------------------
        // TEST
        //---------------------------------------------------------
        if(test)
        {
            if(test_X_file.empty())
            {
                std::cerr << "error in read_param_file(): create specified, but test.X_file.empty()" << '\n';
                //        return false;
                exit(0);
            }

            if(test_H_file.empty())
            {
                std::cerr << "error in read_param_file(): create specified, but test.H_file.empty()" << '\n';
                //        return false;
                exit(0);
            }
        }
*/
    }
    catch(std::exception& e)
    {
        std::cerr << "error in read_param_file(): " << e.what() << '\n';
//        return false;
        exit(0);
    }
    catch(...)
    {
        std::cerr << "error in read_param_file(): unknown" << '\n';
//        return false;
        exit(0);
    }

    //=========================================================
    // FINALIZE AND RETURN
    //=========================================================

    return std::make_tuple(
                           ensemble_file,
                           // =====
                           mlp_file,
                           // -----
                           filename_mlp_create,
                           // -----
                           mlp_train_X_in_file,
                           mlp_train_X_out_file,
                           filename_mlp_train,
                           // =====
                           create,
                           // -----
                           nmlp,
                           // -----
                           is_classif,
                           // -----
                           ni,
                           no,
                           // =====
                           train,
                           // -----
                           train_X_in_file,
                           train_X_out_file,
                           // -----
                           train_method,
                           // =====
                           optimize,
                           // -----
                           optimize_X_in_file,
                           optimize_X_out_file,
                           // -----
                           optimize_method,
                           // -----
                           Ensemble_gen_err_eps,
                           // -----
                           BMC_N,
                           // =====
                           test,
                           // -----
                           test_X_in_file,
                           test_X_out_file,
                           // -----
                           cutoff,
                           // =====
                           prefix
                           );
}
